package rollingCounter

import (
	"sync"
	"time"
)

type RollingCounter interface {
	//Avg returns the average value in the counter
	Avg() float64
	//Min returns the minimal value in the counter
	Min() float64
	//Max returns the maximum value in the counter
	Max() float64
	//Sum returns the sum value in the counter
	Sum() float64
	//Count returns the count of items in the counter
	Count() float64
	//Calculator applies the f to calculate within the valid buckets in the counter
	Calculator(f func(iter Iterator) float64) float64
	//Add adds the val in current bucket
	Add(val float64)
	//Append appends the val in current bucket
	Append(val float64)
}

var _ RollingCounter = (*rollingCounter)(nil)

//Option modifies the default config of a counter
type Option func(opts *rollingCounterOpts)

type rollingCounterOpts struct {
	size int
	bucketDuration time.Duration
}

//WithSize returns an option that modifies the size of counter
func WithSize(size int) Option{
	return func(opts *rollingCounterOpts) {
		opts.size = size
	}
}

//WithBucketDuration returns an option that modifies the bucketDuration of counter
func WithBucketDuration(duration time.Duration) Option {
	return func(opts *rollingCounterOpts) {
		opts.bucketDuration = duration
	}
}

type rollingCounter struct {
	mu sync.RWMutex
	size int
	window *Window
	offset int
	lastModifyTime time.Time
	bucketDuration time.Duration
}

func New(opts ...Option) RollingCounter {
	option := &rollingCounterOpts{
		size: 10,
		bucketDuration: time.Duration(100000000),
	}
	for _, opt := range opts {
		opt(option)
	}

	return &rollingCounter{
		size:option.size,
		bucketDuration: option.bucketDuration,
		window: NewWindow(option.size),
		lastModifyTime: time.Now(),
		offset: 0,
	}
}

func (r *rollingCounter)timespan() int {
	v := int(time.Since(r.lastModifyTime) / r.bucketDuration)
	if v > -1 {
		return v
	}
	return r.size
}

func (r *rollingCounter)pos() int {
	v := r.timespan()
	//If the time interval is greater than 0,
	//reset the expired bucket first
	if v > 0 {
		r.lastModifyTime = r.lastModifyTime.Add(time.Duration(int(r.bucketDuration) * v))
		offset := r.offset
		oldestOffset := offset + 1
		if v > r.size {
			v = r.size
		}
		tail1, tail2 := oldestOffset + v, 0
		if tail1 > r.size {
			tail1, tail2 = r.size, tail1 - r.size
		}
		for i := oldestOffset; i < tail1; i++ {
			r.window.ResetBucket(i)
			offset = i
		}
		for i := 0; i < tail2; i++ {
			r.window.ResetBucket(i)
			offset = i
		}
		r.offset = offset
	}

	return r.offset
}

func (r *rollingCounter)Calculator(f func(iter Iterator) float64) float64 {
	r.mu.RLock()
	defer r.mu.RUnlock()

	v := r.timespan()
	res := 0.0
	//current window not fully expired
	if v < r.size {
		//number of valid buckets
		count := r.size - v
		//skip expired buckets
		offset := r.offset + 1 + v
		if offset >= r.size {
			offset = offset - r.size
		}
		res = f(Iterator{count: count, cur: &(r.window.buckets[offset])})
	}

	return res
}


func (r *rollingCounter)Sum() float64 {
	return r.Calculator(Sum)
}

func (r *rollingCounter)Min() float64 {
	return r.Calculator(Min)
}

func (r *rollingCounter)Max() float64 {
	return r.Calculator(Max)
}

func (r *rollingCounter)Avg() float64 {
	return r.Calculator(Avg)
}

func (r *rollingCounter)Count() float64 {
	return r.Calculator(Count)
}

func (r *rollingCounter)Add(val float64) {
	r.mu.Lock()
	defer r.mu.Unlock()

	offset := r.pos()
	r.window.Add(offset, val)
}

func (r *rollingCounter)Append(val float64) {
	r.mu.Lock()
	defer r.mu.Unlock()

	offset := r.pos()
	r.window.Append(offset, val)
}


